﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.Threading.Tasks;
using XchangeStreamer.Core.Subscriber;
using XchangeStreamer.Utilities;

namespace XchangeStreamer.Subscriber.SDK
{
    public sealed class Connector : IDisposable
    {
        #region Private Fields

        private static Connector connection;
        private Subscriber subscriber;
        private Retriever retriever;
        private Callback callback;
        private EndpointAddress subscriberAddress;
        private EndpointAddress retrieverAddress;
        private static Dictionary<int, IProcessor> topics;
        private bool subscriberOpened = false;
        private bool retrieverOpened = false;
        private IProcessor processor;
        
        #endregion

        #region Private Const. / Dest.

        private Connector()
        {
            topics = new Dictionary<int, IProcessor>();
        }

        ~Connector()
        {
            this.Disconnect(false);
        }

        #endregion

        #region Public Properties

        public static Connector Connection
        {
            get
            {
                if (connection == null)
                {
                    lock (typeof(Connector))
                    {
                        if (connection == null)
                        {
                            connection = new Connector();
                        }
                    }
                }
                return connection;
            }
        }

        #endregion

        #region Public Methods

        public void Connect(string streamerIP, string streamerPort, string cacheServerIP, string cacheServerPort)
        {
            this.Init(streamerIP, streamerPort, cacheServerIP, cacheServerPort);
            this.subscriber = new Subscriber(new InstanceContext(this.callback), string.IsNullOrEmpty(streamerPort) == true ? Settings.NamedPipeBinding : Settings.TcpBinding, this.subscriberAddress);
            this.retriever = new Retriever(string.IsNullOrEmpty(cacheServerPort) == true ? Settings.NamedPipeBinding : Settings.TcpBinding, this.retrieverAddress);
            this.Start();
            this.subscriber.Connect();
        }

        public void Disconnect()
        {
            this.subscriber.Disconnect();
            this.Disconnect(true);
        }

        public void Subscribe<T, K>(int topicId, Action<K, int, TimeSpan> callback)
        {
            topics.Add(topicId, new Processor<T, K>(callback));
            this.subscriber.Subscribe(topicId);
            Console.WriteLine("Subscribed [{0}].", topicId);
        }

        public void Unsubscribe(int topicId)
        {
            topics.Remove(topicId);
            this.subscriber.Unsubscribe(topicId);
            Console.WriteLine("Unsubscribed [{0}].", topicId);
        }

        #endregion

        #region Private Methods

        private void Init(string streamerIP, string streamerPort, string cacheServerIP, string cacheServerPort)
        {
            this.subscriberAddress = string.IsNullOrEmpty(streamerPort) == true ? new EndpointAddress("net.pipe://" + streamerIP + "/Streamer/Subscriber") :
                new EndpointAddress("net.tcp://" + streamerIP + ":" + streamerPort + "/Streamer/Subscriber");

            this.retrieverAddress = string.IsNullOrEmpty(cacheServerPort) == true ? new EndpointAddress("net.pipe://" + cacheServerIP + "/CacheServer/Retriever") :
                new EndpointAddress("net.tcp://" + cacheServerIP + ":" + cacheServerPort + "/CacheServer/Retriever");
            
            this.callback = new Callback(this.ProcessMessage);
        }

        private void Start()
        {
            this.subscriber.InnerChannel.Opening -= new EventHandler(this.Subscriber_Opening);
            this.subscriber.InnerChannel.Opened -= new EventHandler(this.Subscriber_Opened);
            this.subscriber.InnerChannel.Faulted -= new EventHandler(this.Subscriber_Faulted);

            this.retriever.InnerChannel.Opening -= new EventHandler(this.Retriever_Opening);
            this.retriever.InnerChannel.Opened -= new EventHandler(this.Retriever_Opened);
            this.retriever.InnerChannel.Faulted -= new EventHandler(this.Retriever_Faulted);

            this.subscriber.InnerChannel.Opening += new EventHandler(this.Subscriber_Opening);
            this.subscriber.InnerChannel.Opened += new EventHandler(this.Subscriber_Opened);
            this.subscriber.InnerChannel.Faulted += new EventHandler(this.Subscriber_Faulted);

            this.retriever.InnerChannel.Opening += new EventHandler(this.Retriever_Opening);
            this.retriever.InnerChannel.Opened += new EventHandler(this.Retriever_Opened);
            this.retriever.InnerChannel.Faulted += new EventHandler(this.Retriever_Faulted);

            var subscriber = Task.Factory.StartNew(() =>
            {
                while (!this.subscriberOpened)
                {
                    try
                    {
                        this.subscriber.InnerChannel.Open();
                        this.subscriberOpened = true;
                    }
                    catch (Exception ex) { Console.WriteLine(ex.Message); }
                }
            });

            var retriever = Task.Factory.StartNew(() =>
            {
                while (!this.retrieverOpened)
                {
                    try
                    {
                        this.retriever.InnerChannel.Open();
                        this.retrieverOpened = true;
                    }
                    catch (Exception ex) { Console.WriteLine(ex.Message); }
                }
            });

            Task.WaitAll(subscriber, retriever);
            Console.WriteLine("Connected.");
        }

        void Subscriber_Opening(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber: Opening.");
        }

        void Subscriber_Opened(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber: Opened.");
        }

        private void Subscriber_Faulted(object sender, EventArgs e)
        {
            Console.WriteLine("Subscriber: Faulted");
            if (this.subscriberOpened)
            {
                this.subscriberOpened = false;
                this.Start();
            }
        }

        void Retriever_Opening(object sender, EventArgs e)
        {
            Console.WriteLine("Retriever: Opening.");
        }

        void Retriever_Opened(object sender, EventArgs e)
        {
            Console.WriteLine("Retriever: Opened.");
        }

        private void Retriever_Faulted(object sender, EventArgs e)
        {
            Console.WriteLine("Retriever: Faulted");
            if (this.retrieverOpened)
            {
                this.retrieverOpened = false;
                this.Start();
            }
        }

        private void Disconnect(bool suppressFinalize)
        {
            if (this.subscriber != null)
            {
                this.subscriber.Close();
                this.subscriber = null;
            }

            if (this.retriever != null)
            {
                this.retriever.Close();
                this.retriever = null;
            }

            if (this.callback != null)
            {
                this.callback.Dispose();
                this.callback = null;
            }

            if (suppressFinalize)
            {
                GC.SuppressFinalize(this);
            }
        }

        private void ProcessMessage(System.ServiceModel.Channels.Message message)
        {
            if (topics.TryGetValue(message.Headers.GetHeader<int>(1), out processor))
            {
                var msg = XchangeStreamer.Core.MessageConverter<XchangeStreamer.Core.Message>.FromMessage(message);
                message.Close();
                processor.Run(msg);
            }
            else
            {
                message.Close();
            }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this.Disconnect(true);
        }

        #endregion
    }
}